
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

var zrUtil = require("../../../../zrender/lib/core/util");

var numberUtil = require("../../util/number");

var helper = require("./helper");

var sliderMove = require("../helper/sliderMove");

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/
var each = zrUtil.each;
var asc = numberUtil.asc;
/**
 * Operate single axis.
 * One axis can only operated by one axis operator.
 * Different dataZoomModels may be defined to operate the same axis.
 * (i.e. 'inside' data zoom and 'slider' data zoom components)
 * So dataZoomModels share one axisProxy in that case.
 *
 * @class
 */

var AxisProxy = function (dimName, axisIndex, dataZoomModel, ecModel) {
  /**
   * @private
   * @type {string}
   */
  this._dimName = dimName;
  /**
   * @private
   */

  this._axisIndex = axisIndex;
  /**
   * @private
   * @type {Array.<number>}
   */

  this._valueWindow;
  /**
   * @private
   * @type {Array.<number>}
   */

  this._percentWindow;
  /**
   * @private
   * @type {Array.<number>}
   */

  this._dataExtent;
  /**
   * {minSpan, maxSpan, minValueSpan, maxValueSpan}
   * @private
   * @type {Object}
   */

  this._minMaxSpan;
  /**
   * @readOnly
   * @type {module: echarts/model/Global}
   */

  this.ecModel = ecModel;
  /**
   * @private
   * @type {module: echarts/component/dataZoom/DataZoomModel}
   */

  this._dataZoomModel = dataZoomModel; // /**
  //  * @readOnly
  //  * @private
  //  */
  // this.hasSeriesStacked;
};

AxisProxy.prototype = {
  constructor: AxisProxy,

  /**
   * Whether the axisProxy is hosted by dataZoomModel.
   *
   * @public
   * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
   * @return {boolean}
   */
  hostedBy: function (dataZoomModel) {
    return this._dataZoomModel === dataZoomModel;
  },

  /**
   * @return {Array.<number>} Value can only be NaN or finite value.
   */
  getDataValueWindow: function () {
    return this._valueWindow.slice();
  },

  /**
   * @return {Array.<number>}
   */
  getDataPercentWindow: function () {
    return this._percentWindow.slice();
  },

  /**
   * @public
   * @param {number} axisIndex
   * @return {Array} seriesModels
   */
  getTargetSeriesModels: function () {
    var seriesModels = [];
    var ecModel = this.ecModel;
    ecModel.eachSeries(function (seriesModel) {
      if (helper.isCoordSupported(seriesModel.get('coordinateSystem'))) {
        var dimName = this._dimName;
        var axisModel = ecModel.queryComponents({
          mainType: dimName + 'Axis',
          index: seriesModel.get(dimName + 'AxisIndex'),
          id: seriesModel.get(dimName + 'AxisId')
        })[0];

        if (this._axisIndex === (axisModel && axisModel.componentIndex)) {
          seriesModels.push(seriesModel);
        }
      }
    }, this);
    return seriesModels;
  },
  getAxisModel: function () {
    return this.ecModel.getComponent(this._dimName + 'Axis', this._axisIndex);
  },
  getOtherAxisModel: function () {
    var axisDim = this._dimName;
    var ecModel = this.ecModel;
    var axisModel = this.getAxisModel();
    var isCartesian = axisDim === 'x' || axisDim === 'y';
    var otherAxisDim;
    var coordSysIndexName;

    if (isCartesian) {
      coordSysIndexName = 'gridIndex';
      otherAxisDim = axisDim === 'x' ? 'y' : 'x';
    } else {
      coordSysIndexName = 'polarIndex';
      otherAxisDim = axisDim === 'angle' ? 'radius' : 'angle';
    }

    var foundOtherAxisModel;
    ecModel.eachComponent(otherAxisDim + 'Axis', function (otherAxisModel) {
      if ((otherAxisModel.get(coordSysIndexName) || 0) === (axisModel.get(coordSysIndexName) || 0)) {
        foundOtherAxisModel = otherAxisModel;
      }
    });
    return foundOtherAxisModel;
  },
  getMinMaxSpan: function () {
    return zrUtil.clone(this._minMaxSpan);
  },

  /**
   * Only calculate by given range and this._dataExtent, do not change anything.
   *
   * @param {Object} opt
   * @param {number} [opt.start]
   * @param {number} [opt.end]
   * @param {number} [opt.startValue]
   * @param {number} [opt.endValue]
   */
  calculateDataWindow: function (opt) {
    var dataExtent = this._dataExtent;
    var axisModel = this.getAxisModel();
    var scale = axisModel.axis.scale;

    var rangePropMode = this._dataZoomModel.getRangePropMode();

    var percentExtent = [0, 100];
    var percentWindow = [];
    var valueWindow = [];
    var hasPropModeValue;
    each(['start', 'end'], function (prop, idx) {
      var boundPercent = opt[prop];
      var boundValue = opt[prop + 'Value']; // Notice: dataZoom is based either on `percentProp` ('start', 'end') or
      // on `valueProp` ('startValue', 'endValue'). (They are based on the data extent
      // but not min/max of axis, which will be calculated by data window then).
      // The former one is suitable for cases that a dataZoom component controls multiple
      // axes with different unit or extent, and the latter one is suitable for accurate
      // zoom by pixel (e.g., in dataZoomSelect).
      // we use `getRangePropMode()` to mark which prop is used. `rangePropMode` is updated
      // only when setOption or dispatchAction, otherwise it remains its original value.
      // (Why not only record `percentProp` and always map to `valueProp`? Because
      // the map `valueProp` -> `percentProp` -> `valueProp` probably not the original
      // `valueProp`. consider two axes constrolled by one dataZoom. They have different
      // data extent. All of values that are overflow the `dataExtent` will be calculated
      // to percent '100%').

      if (rangePropMode[idx] === 'percent') {
        boundPercent == null && (boundPercent = percentExtent[idx]); // Use scale.parse to math round for category or time axis.

        boundValue = scale.parse(numberUtil.linearMap(boundPercent, percentExtent, dataExtent));
      } else {
        hasPropModeValue = true;
        boundValue = boundValue == null ? dataExtent[idx] : scale.parse(boundValue); // Calculating `percent` from `value` may be not accurate, because
        // This calculation can not be inversed, because all of values that
        // are overflow the `dataExtent` will be calculated to percent '100%'

        boundPercent = numberUtil.linearMap(boundValue, dataExtent, percentExtent);
      } // valueWindow[idx] = round(boundValue);
      // percentWindow[idx] = round(boundPercent);


      valueWindow[idx] = boundValue;
      percentWindow[idx] = boundPercent;
    });
    asc(valueWindow);
    asc(percentWindow); // The windows from user calling of `dispatchAction` might be out of the extent,
    // or do not obey the `min/maxSpan`, `min/maxValueSpan`. But we dont restrict window
    // by `zoomLock` here, because we see `zoomLock` just as a interaction constraint,
    // where API is able to initialize/modify the window size even though `zoomLock`
    // specified.

    var spans = this._minMaxSpan;
    hasPropModeValue ? restrictSet(valueWindow, percentWindow, dataExtent, percentExtent, false) : restrictSet(percentWindow, valueWindow, percentExtent, dataExtent, true);

    function restrictSet(fromWindow, toWindow, fromExtent, toExtent, toValue) {
      var suffix = toValue ? 'Span' : 'ValueSpan';
      sliderMove(0, fromWindow, fromExtent, 'all', spans['min' + suffix], spans['max' + suffix]);

      for (var i = 0; i < 2; i++) {
        toWindow[i] = numberUtil.linearMap(fromWindow[i], fromExtent, toExtent, true);
        toValue && (toWindow[i] = scale.parse(toWindow[i]));
      }
    }

    return {
      valueWindow: valueWindow,
      percentWindow: percentWindow
    };
  },

  /**
   * Notice: reset should not be called before series.restoreData() called,
   * so it is recommanded to be called in "process stage" but not "model init
   * stage".
   *
   * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
   */
  reset: function (dataZoomModel) {
    if (dataZoomModel !== this._dataZoomModel) {
      return;
    }

    var targetSeries = this.getTargetSeriesModels(); // Culculate data window and data extent, and record them.

    this._dataExtent = calculateDataExtent(this, this._dimName, targetSeries); // this.hasSeriesStacked = false;
    // each(targetSeries, function (series) {
    // var data = series.getData();
    // var dataDim = data.mapDimension(this._dimName);
    // var stackedDimension = data.getCalculationInfo('stackedDimension');
    // if (stackedDimension && stackedDimension === dataDim) {
    // this.hasSeriesStacked = true;
    // }
    // }, this);
    // `calculateDataWindow` uses min/maxSpan.

    setMinMaxSpan(this);
    var dataWindow = this.calculateDataWindow(dataZoomModel.settledOption);
    this._valueWindow = dataWindow.valueWindow;
    this._percentWindow = dataWindow.percentWindow; // Update axis setting then.

    setAxisModel(this);
  },

  /**
   * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
   */
  restore: function (dataZoomModel) {
    if (dataZoomModel !== this._dataZoomModel) {
      return;
    }

    this._valueWindow = this._percentWindow = null;
    setAxisModel(this, true);
  },

  /**
   * @param {module: echarts/component/dataZoom/DataZoomModel} dataZoomModel
   */
  filterData: function (dataZoomModel, api) {
    if (dataZoomModel !== this._dataZoomModel) {
      return;
    }

    var axisDim = this._dimName;
    var seriesModels = this.getTargetSeriesModels();
    var filterMode = dataZoomModel.get('filterMode');
    var valueWindow = this._valueWindow;

    if (filterMode === 'none') {
      return;
    } // FIXME
    // Toolbox may has dataZoom injected. And if there are stacked bar chart
    // with NaN data, NaN will be filtered and stack will be wrong.
    // So we need to force the mode to be set empty.
    // In fect, it is not a big deal that do not support filterMode-'filter'
    // when using toolbox#dataZoom, utill tooltip#dataZoom support "single axis
    // selection" some day, which might need "adapt to data extent on the
    // otherAxis", which is disabled by filterMode-'empty'.
    // But currently, stack has been fixed to based on value but not index,
    // so this is not an issue any more.
    // var otherAxisModel = this.getOtherAxisModel();
    // if (dataZoomModel.get('$fromToolbox')
    //     && otherAxisModel
    //     && otherAxisModel.hasSeriesStacked
    // ) {
    //     filterMode = 'empty';
    // }
    // TODO
    // filterMode 'weakFilter' and 'empty' is not optimized for huge data yet.


    each(seriesModels, function (seriesModel) {
      var seriesData = seriesModel.getData();
      var dataDims = seriesData.mapDimension(axisDim, true);

      if (!dataDims.length) {
        return;
      }

      if (filterMode === 'weakFilter') {
        seriesData.filterSelf(function (dataIndex) {
          var leftOut;
          var rightOut;
          var hasValue;

          for (var i = 0; i < dataDims.length; i++) {
            var value = seriesData.get(dataDims[i], dataIndex);
            var thisHasValue = !isNaN(value);
            var thisLeftOut = value < valueWindow[0];
            var thisRightOut = value > valueWindow[1];

            if (thisHasValue && !thisLeftOut && !thisRightOut) {
              return true;
            }

            thisHasValue && (hasValue = true);
            thisLeftOut && (leftOut = true);
            thisRightOut && (rightOut = true);
          } // If both left out and right out, do not filter.


          return hasValue && leftOut && rightOut;
        });
      } else {
        each(dataDims, function (dim) {
          if (filterMode === 'empty') {
            seriesModel.setData(seriesData = seriesData.map(dim, function (value) {
              return !isInWindow(value) ? NaN : value;
            }));
          } else {
            var range = {};
            range[dim] = valueWindow; // console.time('select');

            seriesData.selectRange(range); // console.timeEnd('select');
          }
        });
      }

      each(dataDims, function (dim) {
        seriesData.setApproximateExtent(valueWindow, dim);
      });
    });

    function isInWindow(value) {
      return value >= valueWindow[0] && value <= valueWindow[1];
    }
  }
};

function calculateDataExtent(axisProxy, axisDim, seriesModels) {
  var dataExtent = [Infinity, -Infinity];
  each(seriesModels, function (seriesModel) {
    var seriesData = seriesModel.getData();

    if (seriesData) {
      each(seriesData.mapDimension(axisDim, true), function (dim) {
        var seriesExtent = seriesData.getApproximateExtent(dim);
        seriesExtent[0] < dataExtent[0] && (dataExtent[0] = seriesExtent[0]);
        seriesExtent[1] > dataExtent[1] && (dataExtent[1] = seriesExtent[1]);
      });
    }
  });

  if (dataExtent[1] < dataExtent[0]) {
    dataExtent = [NaN, NaN];
  } // It is important to get "consistent" extent when more then one axes is
  // controlled by a `dataZoom`, otherwise those axes will not be synchronized
  // when zooming. But it is difficult to know what is "consistent", considering
  // axes have different type or even different meanings (For example, two
  // time axes are used to compare data of the same date in different years).
  // So basically dataZoom just obtains extent by series.data (in category axis
  // extent can be obtained from axis.data).
  // Nevertheless, user can set min/max/scale on axes to make extent of axes
  // consistent.


  fixExtentByAxis(axisProxy, dataExtent);
  return dataExtent;
}

function fixExtentByAxis(axisProxy, dataExtent) {
  var axisModel = axisProxy.getAxisModel();
  var min = axisModel.getMin(true); // For category axis, if min/max/scale are not set, extent is determined
  // by axis.data by default.

  var isCategoryAxis = axisModel.get('type') === 'category';
  var axisDataLen = isCategoryAxis && axisModel.getCategories().length;

  if (min != null && min !== 'dataMin' && typeof min !== 'function') {
    dataExtent[0] = min;
  } else if (isCategoryAxis) {
    dataExtent[0] = axisDataLen > 0 ? 0 : NaN;
  }

  var max = axisModel.getMax(true);

  if (max != null && max !== 'dataMax' && typeof max !== 'function') {
    dataExtent[1] = max;
  } else if (isCategoryAxis) {
    dataExtent[1] = axisDataLen > 0 ? axisDataLen - 1 : NaN;
  }

  if (!axisModel.get('scale', true)) {
    dataExtent[0] > 0 && (dataExtent[0] = 0);
    dataExtent[1] < 0 && (dataExtent[1] = 0);
  } // For value axis, if min/max/scale are not set, we just use the extent obtained
  // by series data, which may be a little different from the extent calculated by
  // `axisHelper.getScaleExtent`. But the different just affects the experience a
  // little when zooming. So it will not be fixed until some users require it strongly.


  return dataExtent;
}

function setAxisModel(axisProxy, isRestore) {
  var axisModel = axisProxy.getAxisModel();
  var percentWindow = axisProxy._percentWindow;
  var valueWindow = axisProxy._valueWindow;

  if (!percentWindow) {
    return;
  } // [0, 500]: arbitrary value, guess axis extent.


  var precision = numberUtil.getPixelPrecision(valueWindow, [0, 500]);
  precision = Math.min(precision, 20); // isRestore or isFull

  var useOrigin = isRestore || percentWindow[0] === 0 && percentWindow[1] === 100;
  axisModel.setRange(useOrigin ? null : +valueWindow[0].toFixed(precision), useOrigin ? null : +valueWindow[1].toFixed(precision));
}

function setMinMaxSpan(axisProxy) {
  var minMaxSpan = axisProxy._minMaxSpan = {};
  var dataZoomModel = axisProxy._dataZoomModel;
  var dataExtent = axisProxy._dataExtent;
  each(['min', 'max'], function (minMax) {
    var percentSpan = dataZoomModel.get(minMax + 'Span');
    var valueSpan = dataZoomModel.get(minMax + 'ValueSpan');
    valueSpan != null && (valueSpan = axisProxy.getAxisModel().axis.scale.parse(valueSpan)); // minValueSpan and maxValueSpan has higher priority than minSpan and maxSpan

    if (valueSpan != null) {
      percentSpan = numberUtil.linearMap(dataExtent[0] + valueSpan, dataExtent, [0, 100], true);
    } else if (percentSpan != null) {
      valueSpan = numberUtil.linearMap(percentSpan, [0, 100], dataExtent, true) - dataExtent[0];
    }

    minMaxSpan[minMax + 'Span'] = percentSpan;
    minMaxSpan[minMax + 'ValueSpan'] = valueSpan;
  });
}

var _default = AxisProxy;
module.exports = _default;


//////////////////
// WEBPACK FOOTER
// ./~/echarts/lib/component/dataZoom/AxisProxy.js
// module id = 683
// module chunks = 0
